﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace Alderamin.Core
{
    /// <summary>
    /// Handles the SSL connections in a normal http way (Request/Response)
    /// </summary>
    public sealed class HttpsHandler : HttpHandlerBase
    {
        #region Private Fields
        private const string DATA_SPLIT_SYMBOL = "\r\n";

        // The collection of the network streams that process data through https
        private ConcurrentDictionary<string, NetworkStream> _socketStreamCollection;

        private ConcurrentDictionary<string, string> _hostportCollection;

        private BackgroundWorker _sendRequestWorker;

        private IWebProxy _proxy;

        // The lock used to ensure that only one thread can modify the parameters at the same time
        private object _parameterChangingLock;

        // The cookie used to stored the session information
        private string _currentCookie;

        private string _startScriptUrl;

        private string _mainScriptUrl;

        private string _runningScriptUrl;

        private int _interval;

        private int _maxRunnings;

        private int _currentRunnings;
        #endregion

        #region Public Properties
        /// <summary>
        /// Get a value that indicates whether the handler has been started
        /// </summary>
        public bool IsStarted { get; private set; }

        /// <summary>
        /// The key used to identify the user
        /// </summary>
        public string AccessKey { get; set; }

        /// <summary>
        /// The execution interval of sending the requests, in millisecond. Default is 1000 milliseconds (1 second).
        /// </summary>
        public int Interval
        {
            get
            {
                return _interval;
            }
            set
            {
                if (value > 0 && value < int.MaxValue)
                {
                    _interval = value;
                }
            }
        }

        /// <summary>
        /// The maximum number of running scripts can be executed at the same time.Default is 3.
        /// </summary>
        /// <remarks>Increase this value if more concurrent https processes are required.The running processes will not be affected.
        /// Recommended value is 3 to 6.
        /// </remarks>
        /// <exception cref="System.ArgumentOutOfRangeException">Thrown when the value is less than 1</exception>
        public int MaxRunnings
        {
            get
            {
                return _maxRunnings;
            }
            set
            {
                if (value < 1)
                {
                    throw new ArgumentOutOfRangeException();
                }
                _maxRunnings = value;
            }
        }

        /// <summary>
        /// The number of the running processes
        /// </summary>
        public int CurrentRunnings
        {
            get
            {
                lock (_parameterChangingLock)
                {
                    return _currentRunnings;
                }
            }
            private set
            {
                lock (_parameterChangingLock)
                {
                    _currentRunnings = value;
                }
            }
        }

        /// <summary>
        /// The url of the script that initializes the enviroment of handling HTTPS request
        /// </summary>
        public string StartScriptUrl
        {
            get
            {
                return _startScriptUrl;
            }
            set
            {
                _startScriptUrl = value;
            }
        }

        /// <summary>
        /// The url of the script that handles the HTTPS requests
        /// </summary>
        public string MainScriptUrl
        {
            get
            {
                return _mainScriptUrl;
            }
            set
            {
                _mainScriptUrl = value;
            }
        }

        /// <summary>
        /// The url of the script that runs in the background to maintain the 
        /// tcp connection to the destination servers
        /// </summary>
        public string RunningScriptUrl
        {
            get
            {
                return _runningScriptUrl;
            }
            set
            {
                _runningScriptUrl = value;
            }
        }

        /// <summary>
        /// The web proxy used to send and receive the data
        /// </summary>
        public IWebProxy Proxy
        {
            get
            {
                return _proxy;
            }
            set
            {
                _proxy = value;
            }
        }
        #endregion

        #region Constructor
        /// <summary>
        /// Initializes a new instance of the HttpsHandler class with the specified access key and urls
        /// </summary>
        /// <param name="accessKey">The key used to idetifies the user</param>
        /// <param name="startScriptUrl">The script that initializes the variables on the server</param>
        /// <param name="mainScriptUrl">The script that handles the request</param>
        /// <param name="runningScriptUrl">The script that maintains the tcp connection with destination server in the background</param>
        public HttpsHandler(string accessKey, string startScriptUrl, string mainScriptUrl, string runningScriptUrl)
        {
            _socketStreamCollection = new ConcurrentDictionary<string, NetworkStream>();
            _hostportCollection = new ConcurrentDictionary<string, string>();
            _sendRequestWorker = new BackgroundWorker();
            _parameterChangingLock = new object();

            this.AccessKey = accessKey;
            this.IsStarted = false;
            this.Interval = 1000;
            this.Proxy = null;
            this.MaxRunnings = 3;
            this.CurrentRunnings = 0;
            this.StartScriptUrl = startScriptUrl;
            this.MainScriptUrl = mainScriptUrl;
            this.RunningScriptUrl = runningScriptUrl;
        }
        #endregion

        #region Public Methods

        /// <summary>
        /// Start sending the queued requests and receiving the responses
        /// </summary>
        /// <returns>True if the handler starts successfully;otherwise, false</returns>
        public bool Start()
        {
            if (this.IsStarted || String.IsNullOrWhiteSpace(this.AccessKey) || String.IsNullOrEmpty(_startScriptUrl)
                || String.IsNullOrEmpty(_mainScriptUrl) || String.IsNullOrEmpty(_runningScriptUrl))
            {
                return false;
            }

            // Make a http web request to verify the key and retreive the cookie
            try
            {
                HttpWebRequest verificationRequest = WebRequest.Create(_startScriptUrl + "?key=" + this.AccessKey) as HttpWebRequest;
                verificationRequest.Method = "GET";
                verificationRequest.Proxy = _proxy;
                using (HttpWebResponse resp = verificationRequest.GetResponse() as HttpWebResponse)
                {
                    if (!CheckStatusCode(resp.StatusCode))
                        return false;

                    _currentCookie = resp.Headers[HttpResponseHeader.SetCookie];

                    if (String.IsNullOrEmpty(_currentCookie))
                        return false;
                }
            }
            catch (UriFormatException)
            {
                return false;
            }
            catch (WebException)
            {
                return false;
            }

            _sendRequestWorker.WorkerSupportsCancellation = true;
            _sendRequestWorker.DoWork += (sender, arg) =>
            {
                while (!_sendRequestWorker.CancellationPending)
                {
                    // Check if there is socket stream to be processed
                    if (_socketStreamCollection.Count > 0)
                    {
                        // Check if there are new https connections to be established
                        if (_hostportCollection.Count > 0 && _currentRunnings < _maxRunnings)
                        {
                            ActivateRunningScript();
                        }
                        SendRequests();
                    }
                    Thread.Sleep(50);
                }
            };
            _sendRequestWorker.RunWorkerAsync();

            this.IsStarted = true;
            return true;
        }

        /// <summary>
        /// Stop sending the reuqests and receiving the responses
        /// </summary>
        public void Stop()
        {
            if (this.IsStarted)
            {
                _sendRequestWorker.CancelAsync();
                // Removes all the queued requests
                _hostportCollection.Clear();
                foreach (KeyValuePair<string, NetworkStream> item in _socketStreamCollection)
                {
                    item.Value.Close();
                }
                _socketStreamCollection.Clear();
                this.IsStarted = false;
            }
        }

        /// <summary>
        /// Add a network stream
        /// </summary>
        /// <param name="socketStream">A network stream that handles data through https</param>
        /// <param name="hostport">The host (with port number) to connect</param>
        /// <returns>True if the network stream is vaild and successfully added to the collection;otherwise, false</returns>
        public bool AddStream(NetworkStream socketStream, string hostport)
        {
            if (null == socketStream || !socketStream.CanRead || !socketStream.CanWrite)
            {
                return false;
            }

            string id = GenerateUniqueID();
            if (_socketStreamCollection.TryAdd(id, socketStream))
            {
                if (_hostportCollection.TryAdd(id, hostport))
                {
                    return true;
                }
                RemoveStream(id);
            }
            return false;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Send the queued requests and decode the response
        /// </summary>
        private void SendRequests()
        {
            HttpWebRequest queryRequest = base.CreatePostRequest(_mainScriptUrl, _proxy, _currentCookie);
            if (null == queryRequest)
                return;

            try
            {
                // Get all the queued data and write into the request stream
                MemoryStream combinedDataStream = GetCombinedDataStream();
                if (combinedDataStream.Length > 0)
                {
                    queryRequest.ContentLength = combinedDataStream.Length;
                    combinedDataStream.WriteTo(queryRequest.GetRequestStream());
                    queryRequest.GetRequestStream().Close();
                }
                else
                {
                    Thread.Sleep(_interval);
                }

                // Read the data line by line in order to get the id and corressponding response
                using (BinaryReader binReader = new BinaryReader(queryRequest.GetResponse().GetResponseStream()))
                {
                    string id = null;
                    // Loop to split all the response data
                    while (!String.IsNullOrEmpty(id = ExtractString(binReader)))
                    {
                        int bytesLength;
                        if (Int32.TryParse(ExtractString(binReader), out bytesLength))
                        {
                            NetworkStream writeBackStream;
                            if (_socketStreamCollection.TryGetValue(id, out writeBackStream))
                            {
                                byte[] respBytes = binReader.ReadBytes(bytesLength);
                                if (writeBackStream.CanWrite)
                                {
                                    writeBackStream.Write(respBytes, 0, respBytes.Length);
                                }
                                else
                                {
                                    RemoveStream(id);
                                }

                            }
                        }
                    }
                }
            }
            catch (WebException) { }
            catch (IOException) { }
        }

        /// <summary>
        /// Get a stream that encapsulates all the request data encoded in the format: "id\r\ndata\r\n"
        /// </summary>
        /// <returns></returns>
        private MemoryStream GetCombinedDataStream()
        {
            byte[] crBytes = Encoding.ASCII.GetBytes(DATA_SPLIT_SYMBOL);
            List<string> notReadableStream = new List<string>();
            MemoryStream combinedDataStream = new MemoryStream();

            foreach (KeyValuePair<string, NetworkStream> request in _socketStreamCollection)
            {
                NetworkStream ns = request.Value;
                // Check if the current socket stream has data to send
                if (ns.CanRead && ns.DataAvailable)
                {
                    byte[] idBytes = Encoding.ASCII.GetBytes(request.Key); // Get the corresponding id
                    combinedDataStream.Write(idBytes, 0, idBytes.Length); // Write the id bytes
                    combinedDataStream.Write(crBytes, 0, crBytes.Length); // Write the CR symbol bytes

                    // Loop to get all the pending data
                    int bytesRead;
                    byte[] requestData = new byte[8192];
                    do
                    {
                        bytesRead = ns.Read(requestData, 0, requestData.Length);
                        combinedDataStream.Write(requestData, 0, bytesRead); // Write the data bytes
                    }
                    while (ns.DataAvailable);

                    combinedDataStream.Write(crBytes, 0, crBytes.Length); // Write the CR symbol bytes
                }
                else if (!ns.CanRead)
                {
                    notReadableStream.Add(request.Key);
                }
            }

            // Removes the stream that are closed
            foreach (string id in notReadableStream)
            {
                RemoveStream(id);
            }
            return combinedDataStream;
        }

        /// <summary>
        /// Get the data before the specified symbol is encountered
        /// </summary>
        /// <param name="binReader"></param>
        /// <returns></returns>
        private String ExtractString(BinaryReader binReader)
        {
            StringBuilder sb = new StringBuilder();
            try
            {
                string remainChars = null;
                while (true)
                {
                    char c = binReader.ReadChar();
                    // If the current character is equal to the first character in DATA_SPLIT_SYMBOL,
                    // then compare the rest of following characters
                    if (DATA_SPLIT_SYMBOL[0].CompareTo(c) == 0)
                    {
                        remainChars = new string(binReader.ReadChars(DATA_SPLIT_SYMBOL.Length - 1));
                        if (DATA_SPLIT_SYMBOL.Contains(remainChars))
                            break;
                    }
                    sb.Append(c);
                    if (String.IsNullOrEmpty(remainChars))
                    {
                        sb.Append(remainChars);
                    }
                }
            }
            catch (ArgumentException) { sb.Clear(); }
            catch (IOException) { sb.Clear(); }

            return sb.ToString();
        }

        /// <summary>
        /// Activate a connection handling script with the destination server address
        /// </summary>
        private void ActivateRunningScript()
        {
            // Construst the data that contains the information of all the pending https connections
            List<string> activeIdCollection = new List<string>();
            Byte[] respToClient = Encoding.ASCII.GetBytes("HTTP/1.0 200 Connection established\r\n\r\n");
            StringBuilder requestBuilder = new StringBuilder();
            foreach (KeyValuePair<string, string> hostport in _hostportCollection)
            {
                requestBuilder.Append(hostport.Key); // The id
                requestBuilder.Append(DATA_SPLIT_SYMBOL); // The delimeter
                requestBuilder.Append(hostport.Value); // The remote host and port
                requestBuilder.Append(DATA_SPLIT_SYMBOL); // The delimeter
                activeIdCollection.Add(hostport.Key);
                // Notifies the client that the tunnel has been established
                _socketStreamCollection[hostport.Key].Write(respToClient, 0, respToClient.Length);
            }
            _hostportCollection.Clear();

            if (requestBuilder.Length > 0)
            {
                HttpWebRequest activateRequest = CreatePostRequest(_runningScriptUrl, _proxy, _currentCookie);
                if (null == activateRequest)
                    return;

                BackgroundWorker bw = new BackgroundWorker();
                bw.DoWork += (sender, arg) =>
                {
                    this.CurrentRunnings++;

                    try
                    {
                        byte[] requestData = Encoding.ASCII.GetBytes(requestBuilder.ToString());
                        activateRequest.ContentLength = requestData.Length;
                        activateRequest.GetRequestStream().Write(requestData, 0, requestData.Length);
                        activateRequest.GetRequestStream().Close();

                        HttpWebResponse webresp = activateRequest.GetResponse() as HttpWebResponse;
                        webresp.Close();
                    }
                    catch (WebException) { }

                    // Remove all the corresponding socket stream
                    List<string> idCollection = arg.Argument as List<string>;
                    foreach (string id in idCollection)
                    {
                        RemoveStream(id);
                    }

                    this.CurrentRunnings--;
                };
                bw.RunWorkerAsync(activeIdCollection);
            }
        }

        /// <summary>
        /// Remove the stream from the collection with the specified id
        /// </summary>
        /// <param name="id"></param>        
        private void RemoveStream(string id)
        {
            NetworkStream removedStream;
            if (_socketStreamCollection.TryRemove(id, out removedStream))
            {
                removedStream.Close();
            }
        }

        #endregion
    }
}
